<?php

abstract class Jf_Db_Table_Abstract {

    protected static $_defaultDb;

    protected $_db;

    protected $_schema = null;

    protected $_name = null;

    protected $_cols;

    protected $_dbTableCols = array();

    protected $_primary = null;

    protected $_select = null;

    protected $_metadata = array();

    protected $_dbTableMetadata = array();

    protected $_metadataCache = false;

    protected $_defaultOrder = array();

    public function __construct($config = array()) {
        if (! is_array($config)) {
            $config = array(
                self::ADAPTER => $config
            );
        }
        if ($config) {
            $this->setOptions($config);
        }
        $this->_setup();
        $this->init();
    }

    public function init() {}

    protected function _setup() {
        $this->_setupDatabaseAdapter();
        $this->_setupTableName();
        $this->_setupSelect();
    }

    public static function getDefaultAdapter() {
        return self::$_defaultDb;
    }

    public static function setDefaultAdapter($db = null) {
        self::$_defaultDb = self::_setupAdapter($db);
    }

    protected function _setAdapter($db) {
        $this->_db = self::_setupAdapter($db);
        return $this;
    }

    public function getAdapter() {
        return $this->_db;
    }

    protected function _setupDatabaseAdapter() {
        if (! $this->_db) {
            $this->_db = self::getDefaultAdapter();
            if (! $this->_db instanceof Zend_Db_Adapter_Abstract) {
                throw new Jf_Exception('No adapter found for ' . get_class($this));
            }
        }
    }

    protected static function _setupAdapter($db) {
        if ($db === null) {
            return null;
        }
        if (is_string($db)) {
            $db = Jf_Registry::get($db);
        }
        if (! $db instanceof Zend_Db_Adapter_Abstract) {
            throw new Jf_Exception('Argument must be of type Zend_Db_Adapter_Abstract, or a Registry key where a Zend_Db_Adapter_Abstract object is stored');
        }
        return $db;
    }

    protected function _setupTableName() {
        if (! $this->_name) {
            $this->_name = get_class($this);
        } else if (strpos($this->_name, '.')) {
            list ($this->_schema, $this->_name) = explode('.', $this->_name);
        }
        $this->_name = $this->_getDbPrefix() . $this->_name;
    }

    protected function _setupSelect() {
        if (! $this->_select) {
            $this->_select = $this->_db->select();
        }
    }

    protected function _getCols() {
        if (null === $this->_cols) {
            $this->_setupMetadata();
            $this->_cols = array_keys($this->_metadata);
        }
        return $this->_cols;
    }

    protected function _getDbTableCols($dbTable) {
        if (empty($this->_dbTableCols[$dbTable])) {
            $this->_setupDbTableMetadata($dbTable);
            $this->_dbTableCols[$dbTable] = array_keys($this->_dbTableMetadata[$dbTable]);
        }
        return $this->_dbTableCols[$dbTable];
    }

    protected function _setupMetadata() {
        if (count($this->_metadata) > 0) {
            return true;
        } elseif ($this->_metadataCache) {
            $cacheKey = md5(get_Class($this) . '_meta');
            if (! ($metadata = Jf::cache()->get($cacheKey))) {
                $metadata = $this->_db->describeTable($this->_name, $this->_schema);
                Jf::cache()->set($cacheKey, $metadata);
            }
            $this->_metadata = $metadata;
        } else {
            $this->_metadata = $this->_db->describeTable($this->_name, $this->_schema);
        }
    }

    protected function _setupDbTableMetadata($dbTable) {
        if (isset($this->_dbTableMetadata[$dbTable])) {
            return true;
        } elseif ($this->_metadataCache) {
            $cacheKey = md5($dbTable . '_meta');
            if (! ($metadata = Jf::cache()->get($cacheKey))) {
                $metadata = $this->_db->describeTable($dbTable, $this->_schema);
                Jf::cache()->set($cacheKey, $metadata);
            }
            $this->_dbTableMetadata[$dbTable] = $metadata;
        } else {
            $this->_dbTableMetadata[$dbTable] = $this->_db->describeTable($dbTable, $this->_schema);
        }
    }

    public function getPrimary() {
        return $this->_primary;
    }

    public function getSelect() {
        return $this->_select;
    }

    protected function _getDbPrefix() {
        $config = $this->_db->getConfig();
        return isset($config['prefix']) && trim($config['prefix']) ? $config['prefix'] : '';
    }

    public function getDbTable($dbTable) {
        $dbTable = $this->_getDbPrefix() . $dbTable;
        return $dbTable;
    }

    protected function _getTableName() {
        return $this->_name;
    }

    public function truncateTable() {
        $tableName = $this->_getTableName();
        $this->_db->query("TRUNCATE TABLE $tableName");
    }

    public function totalCount() {
        $this->_select->reset(Zend_Db_Select::COLUMNS)->reset(Zend_Db_Select::ORDER)->reset(Zend_Db_Select::LIMIT_OFFSET)->reset(Zend_Db_Select::DISTINCT)->columns("COUNT($this->_primary)");
        $sql = $this->_select->assemble();
        return $this->_db->fetchOne($sql, $this->_bind);
    }

    public function getCount($where) {
        $this->_select->reset();
        $this->_select->from($this->_name, "COUNT($this->_primary)");
        if (isset($where)) {
            $this->_where($where);
        }
        $sql = $this->_select->assemble();
        return $this->_db->fetchOne($sql);
    }

    public function fetchPairs($columns, $where) {
        $this->_select->reset();
        $this->_select->from($this->_name, $this->getIntersectCols($columns));
        $this->_where($where);
        $sql = $this->_select->assemble();
        return $this->_db->fetchPairs($sql);
    }

    public function getIntersectCols($cols) {
        if ($cols === '*') {
            return $cols;
        } else {
            $intersectCols = array_intersect((array) $cols, $this->_getCols());
            if (! $intersectCols) {
                $noFoundCols = implode(',', (array) $cols);
                throw new Jf_Exception("columns no in this table: '$noFoundCols'");
            }
            return $intersectCols;
        }
    }

    public function fetchAll($columns = '*', $where = array(), $order = null, $count = null, $offset = null) {
        $this->_select->reset();
        $this->_select->from($this->_name, $this->getIntersectCols($columns));
        if ($where !== null) {
            $this->_where($where);
        }
        if ($order !== null) {
            $this->_order($order);
        } elseif ($this->_defaultOrder) {
            $this->_order($this->_defaultOrder);
        }
        $this->_order("$this->_primary desc");
        if ($count !== null || $offset !== null) {
            $this->_select->limit($count, $offset);
        }
        $sql = $this->_select->assemble();
        $rows = $this->_db->fetchAll($sql, $this->_bind);
        return $rows;
    }

    public function fetchCol($column, $where = array(), $order = null, $count = null, $offset = null) {
        $this->_select->reset();
        $this->_select->from($this->_name, $this->getIntersectCols($column));
        if ($where !== null) {
            $this->_where($where);
        }
        $this->_order("$this->_primary desc");
        if ($order !== null) {
            $this->_order($order);
        } elseif ($this->_default_order) {
            $this->_order($this->_default_order);
        }
        if ($count !== null || $offset !== null) {
            $this->_select->limit($count, $offset);
        }
        $sql = $this->_select->assemble();
        return $this->_db->fetchCol($sql);
    }

    public function fetchCols($columns = array(), $where = array(), $order = null, $count = null, $offset = null) {
        $result = array();
        if (version_compare(PHP_VERSION, '5.5.0') >= 0) {
            $rows = $this->fetchAll($where, $columns, $order, $count, $offset);
            foreach ($columns as $column) {
                $result[$column] = array_column($rows, $column);
            }
        } else {
            foreach ($columns as $column) {
                $result[$column] = $this->fetchCol($column, $where, $order, $count, $offset);
            }
        }
        return $result;
    }

    protected function _where($where) {
        $where = (array) $where;
        foreach ($where as $key => $val) {
            if (is_int($key)) {
                $this->_select->where($val);
            } else {
                $this->_select->where($key, $val);
            }
        }
    }

    public function updateByPrimary(array $data, $id) {
        if (! Zend_Validate::is($id, 'Int')) {
            throw new Jf_Exception('primary not Int');
        }
        $this->_setupMetadata();
        $data = array_intersect_key($data, $this->_metadata);
        $tableSpec = ($this->_schema ? $this->_schema . '.' : '') . $this->_name;
        $result = $this->_db->update($tableSpec, $data, array(
            "$this->_primary = ?" => $id
        ));
        return $result;
    }

    protected function _order($order) {
        if (! is_array($order)) {
            $order = array(
                $order
            );
        }
        foreach ($order as $val) {
            $this->_select->order($val);
        }
    }

    public function insert(array $data) {
        $this->_setupMetadata();
        $data = array_intersect_key($data, $this->_metadata);
        $tableSpec = ($this->_schema ? $this->_schema . '.' : '') . $this->_name;
        $this->_db->insert($tableSpec, $data);
        return $this->_db->lastInsertId();
    }

    public function update(array $data, $where) {
        $this->_setupMetadata();
        $data = array_intersect_key($data, $this->_metadata);
        $tableSpec = ($this->_schema ? $this->_schema . '.' : '') . $this->_name;
        return $this->_db->update($tableSpec, $data, $where);
    }

    public function batchUpdate(array $data, $id) {
        return $this->update($data, "$this->_primary in ($id)");
    }

    public function batchDelete($data) {
        $tableSpec = ($this->_schema ? $this->_schema . '.' : '') . $this->_name;
        $stmt = $this->_db->query("DELETE FROM $tableSpec WHERE `$this->_primary` in ($data)");
        $result = $stmt->rowCount();
        return $result;
    }

    public function delete($where) {
        $tableSpec = ($this->_schema ? $this->_schema . '.' : '') . $this->_name;
        return $this->_db->delete($tableSpec, $where);
    }

    public function deleteByPrimary($primary) {
        // $this->_beforeDelete($primary);
        $tableSpec = ($this->_schema ? $this->_schema . '.' : '') . $this->_name;
        $where = $this->_db->quoteInto("$this->_primary = ?", $primary);
        $result = $this->_db->delete($tableSpec, $where);
        return $result;
    }

    public function find($id, $columns = '*') {
        $this->_select->reset();
        $this->_select->from($this->_name, $this->getIntersectCols($columns));
        $where = $this->_db->quoteInto("$this->_primary  =?", $id);
        $this->_select->where($where);
        $sql = $this->_select->assemble();
        return $this->_db->fetchRow($sql);
    }

    public function findByPrimary($columns = '*', $id) {
        $this->_select->reset();
        $this->_select->from($this->_name, $this->getIntersectCols($columns));
        $where = $this->_db->quoteInto("$this->_primary  =?", $id);
        $this->_select->where($where);
        $sql = $this->_select->assemble();
        return $this->_db->fetchRow($sql);
    }

    public function fetchRow($columns = '*', $where = null) {
        $this->_select->reset();
        $this->_select->from($this->_name, $this->getIntersectCols($columns));
        if ($where !== null) {
            $this->_where($where);
        }
        $sql = $this->_select->assemble();
        return $this->_db->fetchRow($sql);
    }

    public function fetchOne($columns, $where) {
        $this->_select->reset();
        $this->_select->from($this->_name, $this->getIntersectCols($columns));
        if ($where !== null) {
            $this->_where($where);
        }
        $sql = $this->_select->assemble();
        return $this->_db->fetchOne($sql);
    }

    public function fetchOneByPrimary($columns, $priamry) {
        $this->_select->reset();
        $this->_select->from($this->_name, $this->getIntersectCols($columns));
        $this->_select->where("$this->_primary = $priamry");
        $sql = $this->_select->assemble();
        return $this->_db->fetchOne($sql);
    }

    public function checkEqual($key, $value, $where = null) {
        $this->_select->reset();
        $this->_select->from($this->_name, $this->getIntersectCols($key));
        $this->_select->where($this->_db->quoteInto("$key =?", $value));
        if (isset($where)) {
            $this->_where($where);
        }
        $sql = $this->_select->assemble();
        $result = $this->_db->fetchOne($sql);
        return $result ? TRUE : FALSE;
    }

    public function findOneByPrimary($columns, $id) {
        $select = $this->_db->select();
        $select->from($this->_name, $this->getIntersectCols($columns));
        $select->where($this->_db->quoteInto("$this->_primary  = ?", $id));
        $sql = $select->assemble();
        $result = $this->_db->fetchOne($sql);
        return $result;
    }

    public function insertOnDuplicate($insertData, $updateFields = null) {
        $data[] = $insertData;
        return $this->batchInsert($data, $updateFields);
    }

    public function batchInsert($data, $updateFields = null) {
        $table = $this->_getTableName();
        $data = $this->_processBatchInsetData($data);
        $columns = $data['columns'];
        $values = $data['values'];
        $insertSql = sprintf("INSERT INTO %s (%s) VALUES %s", $this->_db->quoteIdentifier($table), $columns, $values);
        if (isset($updateFields)) {
            $fields = '';
            foreach ((array) $updateFields as $key => $value) {
                $fields .= "$value = " . sprintf('VALUES(%s)', $this->_db->quoteIdentifier($value));
                if ($key != count($updateFields) - 1) {
                    $fields .= ', ';
                }
            }
            $insertSql .= ' ON DUPLICATE KEY UPDATE ' . $fields;
        }
        $result = $this->_db->query($insertSql, $data['bind']);
        return $result;
    }

    protected function _processBatchInsetData($data) {
        $bind = array();
        $values = array();
        foreach ($data as $row) {
            $line = array();
            foreach ($row as $value) {
                $line[] = '?';
                $bind[] = $value;
            }
            $columns = $this->getIntersectCols(array_keys($row));
            $values[] = sprintf('(%s)', implode(', ', $line));
        }
        $data = array(
            'columns' => implode(',', array_map(array(
                $this->_db,
                'quoteIdentifier'
            ), $columns)),
            'values' => implode(', ', $values),
            'bind' => $bind
        );
        return $data;
    }

    public function getPaginator($perPage, $page, $where = null, $attributes = '*', $order = null) {
        $offset = ($page > 0) ? ($page - 1) * $perPage : 0;
        $rows = $this->fetchAll($attributes, $where, $order, $perPage, $offset);
        $result = new Jf_RecordSet($rows);
        $count = $this->totalCount();
        $adapter = new Jf_Paginator_Adapter_Iterator($result, $count);
        $paginator = new Zend_Paginator($adapter);
        $paginator->setItemCountPerPage($perPage)->setCurrentPageNumber($page);
        return $paginator;
    }
}
